/*
 * Copyright (c) 2017, cxy7.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.b3log.solo.controller;

import java.text.ParseException;
import java.util.Date;
import java.util.List;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.b3log.solo.Keys;
import org.b3log.solo.Latkes;
import org.b3log.solo.model.Article;
import org.b3log.solo.model.Option;
import org.b3log.solo.model.Tag;
import org.b3log.solo.model.User;
import org.b3log.solo.renderer.TextXMLRenderer;
import org.b3log.solo.service.ArticleMgmtService;
import org.b3log.solo.service.ArticleQueryService;
import org.b3log.solo.service.PreferenceQueryService;
import org.b3log.solo.service.ServiceException;
import org.b3log.solo.service.TagQueryService;
import org.b3log.solo.service.UserQueryService;
import org.b3log.solo.util.MD5;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.XML;
import org.jsoup.Jsoup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * <a href="http://www.xmlrpc.com/metaWeblogApi">MetaWeblog API</a> requests
 * processing.
 *
 * <p>
 * Implemented the following APIs:
 * <ul>
 * <li>blogger.deletePost</li>
 * <li>blogger.getUsersBlogs</li>
 * <li>metaWeblog.editPost</li>
 * <li>metaWeblog.getCategories</li>
 * <li>metaWeblog.getPost</li>
 * <li>metaWeblog.getRecentPosts</li>
 * <li>metaWeblog.newPost</li>
 * </ul>
 * </p>
 *
 * @author <a href="http://cxy7.com">XyCai</a>
 * @version 1.0.0.12, Nov 20, 2015
 * @since 0.4.0
 */
@Controller
public class MetaWeblogAPI {

	/**
	 * Logger.
	 */
	private static Logger logger = LoggerFactory.getLogger(MetaWeblogAPI.class);

	/**
	 * Preference query service.
	 */
	@Autowired
	private PreferenceQueryService preferenceQueryService;

	/**
	 * Tag query service.
	 */
	@Autowired
	private TagQueryService tagQueryService;

	/**
	 * Article query service.
	 */
	@Autowired
	private ArticleQueryService articleQueryService;

	/**
	 * Article management service.
	 */
	@Autowired
	private ArticleMgmtService articleMgmtService;

	/**
	 * User query service.
	 */
	@Autowired
	private UserQueryService userQueryService;

	/**
	 * Key of method call.
	 */
	private static final String METHOD_CALL = "methodCall";

	/**
	 * Key of method name.
	 */
	private static final String METHOD_NAME = "methodName";

	/**
	 * Method name: "blogger.getUsersBlogs".
	 */
	private static final String METHOD_GET_USERS_BLOGS = "blogger.getUsersBlogs";

	/**
	 * Method name: "metaWeblog.getCategories".
	 */
	private static final String METHOD_GET_CATEGORIES = "metaWeblog.getCategories";

	/**
	 * Method name: "metaWeblog.getRecentPosts".
	 */
	private static final String METHOD_GET_RECENT_POSTS = "metaWeblog.getRecentPosts";

	/**
	 * Method name: "metaWeblog.newPost".
	 */
	private static final String METHOD_NEW_POST = "metaWeblog.newPost";

	/**
	 * Method name: "metaWeblog.editPost".
	 */
	private static final String METHOD_EDIT_POST = "metaWeblog.editPost";

	/**
	 * Method name: "metaWeblog.getPost".
	 */
	private static final String METHOD_GET_POST = "metaWeblog.getPost";

	/**
	 * Method name: "blogger.deletePost".
	 */
	private static final String METHOD_DELETE_POST = "blogger.deletePost";

	/**
	 * Argument "username" index.
	 */
	private static final int INDEX_USER_EMAIL = 1;

	/**
	 * Argument "postid" index.
	 */
	private static final int INDEX_POST_ID = 0;

	/**
	 * Argument "password" index.
	 */
	private static final int INDEX_USER_PWD = 2;

	/**
	 * Argument "numberOfPosts" index.
	 */
	private static final int INDEX_NUM_OF_POSTS = 3;

	/**
	 * Argument "post" index.
	 */
	private static final int INDEX_POST = 3;

	/**
	 * Argument "publish" index.
	 */
	private static final int INDEX_PUBLISH = 4;

	/**
	 * Article abstract length.
	 */
	private static final int ARTICLE_ABSTRACT_LENGTH = 500;

	/**
	 * MetaWeblog requests processing.
	 *
	 * @param request
	 *            the specified http servlet request
	 * @param response
	 *            the specified http servlet response
	 * @param context
	 *            the specified http request context
	 */
	@RequestMapping(value = "/apis/metaweblog", method = RequestMethod.POST)
	public void metaWeblog(final HttpServletRequest request, final HttpServletResponse response) {
		final TextXMLRenderer renderer = new TextXMLRenderer();
		String responseContent = null;

		try {
			final ServletInputStream inputStream = request.getInputStream();
			final String xml = IOUtils.toString(inputStream, "UTF-8");
			final JSONObject requestJSONObject = XML.toJSONObject(xml);

			final JSONObject methodCall = requestJSONObject.getJSONObject(METHOD_CALL);
			final String methodName = methodCall.getString(METHOD_NAME);

			logger.info("MetaWeblog[methodName={}]", methodName);

			final JSONArray params = methodCall.getJSONObject("params").getJSONArray("param");

			if (METHOD_DELETE_POST.equals(methodName)) {
				params.remove(0); // Removes the first argument "appkey"
			}

			final String userEmail = params.getJSONObject(INDEX_USER_EMAIL).getJSONObject("value").getString("string");
			final JSONObject user = userQueryService.getUserByEmail(userEmail);

			if (null == user) {
				throw new Exception("No user[email=" + userEmail + "]");
			}

			final String userPwd = params.getJSONObject(INDEX_USER_PWD).getJSONObject("value").getString("string");

			if (!user.getString(User.USER_PASSWORD).equals(MD5.hash(userPwd))) {
				throw new Exception("Wrong password");
			}

			if (METHOD_GET_USERS_BLOGS.equals(methodName)) {
				responseContent = getUsersBlogs();
			} else if (METHOD_GET_CATEGORIES.equals(methodName)) {
				responseContent = getCategories();
			} else if (METHOD_GET_RECENT_POSTS.equals(methodName)) {
				final int numOfPosts = params.getJSONObject(INDEX_NUM_OF_POSTS).getJSONObject("value").getInt("int");

				responseContent = getRecentPosts(numOfPosts);
			} else if (METHOD_NEW_POST.equals(methodName)) {
				final JSONObject article = parsetPost(methodCall);

				article.put(Article.ARTICLE_AUTHOR_EMAIL, userEmail);
				addArticle(article);

				final StringBuilder stringBuilder = new StringBuilder(
						"<?xml version=\"1.0\" encoding=\"UTF-8\"?><methodResponse>")
								.append("<params><param><value><string>").append(article.getString(Keys.OBJECT_ID))
								.append("</string></value></param></params></methodResponse>");

				responseContent = stringBuilder.toString();
			} else if (METHOD_GET_POST.equals(methodName)) {
				final String postId = params.getJSONObject(INDEX_POST_ID).getJSONObject("value").getString("string");

				responseContent = getPost(postId);
			} else if (METHOD_EDIT_POST.equals(methodName)) {
				final JSONObject article = parsetPost(methodCall);
				final String postId = params.getJSONObject(INDEX_POST_ID).getJSONObject("value").getString("string");

				article.put(Keys.OBJECT_ID, postId);

				article.put(Article.ARTICLE_AUTHOR_EMAIL, userEmail);
				final JSONObject updateArticleRequest = new JSONObject();

				updateArticleRequest.put(Article.ARTICLE, article);
				articleMgmtService.updateArticle(updateArticleRequest);

				final StringBuilder stringBuilder = new StringBuilder(
						"<?xml version=\"1.0\" encoding=\"UTF-8\"?><methodResponse>")
								.append("<params><param><value><string>").append(postId)
								.append("</string></value></param></params></methodResponse>");

				responseContent = stringBuilder.toString();
			} else if (METHOD_DELETE_POST.equals(methodName)) {
				final String postId = params.getJSONObject(INDEX_POST_ID).getJSONObject("value").getString("string");

				articleMgmtService.removeArticle(postId);

				final StringBuilder stringBuilder = new StringBuilder(
						"<?xml version=\"1.0\" encoding=\"UTF-8\"?><methodResponse>")
								.append("<params><param><value><boolean>").append(true)
								.append("</boolean></value></param></params></methodResponse>");

				responseContent = stringBuilder.toString();
			} else {
				throw new UnsupportedOperationException("Unsupported method[name=" + methodName + "]");
			}
		} catch (final Exception e) {
			logger.error(e.getMessage(), e);

			final StringBuilder stringBuilder = new StringBuilder(
					"<?xml version=\"1.0\" encoding=\"UTF-8\"?><methodResponse>").append("<fault><value><struct>")
							.append("<member><name>faultCode</name><value><int>500</int></value></member>")
							.append("<member><name>faultString</name><value><string>").append(e.getMessage())
							.append("</string></value></member></struct></value></fault></methodResponse>");

			responseContent = stringBuilder.toString();
		}

		renderer.setContent(responseContent);
		renderer.render(request, response);
	}

	/**
	 * Processes {@value #METHOD_GET_POST}.
	 *
	 * @param postId
	 *            the specified post id
	 * @return method response XML
	 * @throws Exception
	 *             exception
	 */
	private String getPost(final String postId) throws Exception {
		final StringBuilder stringBuilder = new StringBuilder(
				"<?xml version=\"1.0\" encoding=\"UTF-8\"?><methodResponse><params><param><value>");

		final String posts = buildPost(postId);

		stringBuilder.append(posts);

		stringBuilder.append("</value></param></params></methodResponse>");

		return stringBuilder.toString();
	}

	/**
	 * Adds the specified article.
	 *
	 * @param article
	 *            the specified article
	 * @throws Exception
	 *             exception
	 */
	private void addArticle(final JSONObject article) throws Exception {
		// final Transaction transaction = articleDao.beginTransaction();

		try {
			articleMgmtService.addArticleInternal(article);
			// transaction.commit();
		} catch (final ServiceException e) {
			// if (transaction.isActive()) {
			// transaction.rollback();
			// }

			throw e;
		}
	}

	/**
	 * Parses the specified method call for an article.
	 *
	 * @param methodCall
	 *            the specified method call
	 * @return article
	 * @throws Exception
	 *             exception
	 */
	private JSONObject parsetPost(final JSONObject methodCall) throws Exception {
		final JSONObject ret = new JSONObject();

		final JSONArray params = methodCall.getJSONObject("params").getJSONArray("param");
		final JSONObject post = params.getJSONObject(INDEX_POST).getJSONObject("value").getJSONObject("struct");
		final JSONArray members = post.getJSONArray("member");

		for (int i = 0; i < members.length(); i++) {
			final JSONObject member = members.getJSONObject(i);
			final String name = member.getString("name");

			if ("dateCreated".equals(name)) {
				final String dateString = member.getJSONObject("value").getString("dateTime.iso8601");
				Date date;

				try {
					date = (Date) DateFormatUtils.ISO_DATETIME_FORMAT.parseObject(dateString);
				} catch (final ParseException e) {
					logger.warn("Parses article create date failed with ISO8601, retry to parse with "
							+ "pattern[yyyy-MM-dd'T'HH:mm:ss, yyyyMMdd'T'HH:mm:ss'Z']");
					date = DateUtils.parseDate(dateString,
							new String[] { "yyyyMMdd'T'HH:mm:ss", "yyyyMMdd'T'HH:mm:ss'Z'" });
				}
				ret.put(Article.ARTICLE_CREATE_DATE, date);
			} else if ("title".equals(name)) {
				ret.put(Article.ARTICLE_TITLE, member.getJSONObject("value").getString("string"));
			} else if ("description".equals(name)) {
				final String content = member.getJSONObject("value").getString("string");

				ret.put(Article.ARTICLE_CONTENT, content);

				final String plainTextContent = Jsoup.parse(content).text();

				if (plainTextContent.length() > ARTICLE_ABSTRACT_LENGTH) {
					ret.put(Article.ARTICLE_ABSTRACT, plainTextContent.substring(0, ARTICLE_ABSTRACT_LENGTH));
				} else {
					ret.put(Article.ARTICLE_ABSTRACT, plainTextContent);
				}
			} else if ("categories".equals(name)) {
				final StringBuilder tagBuilder = new StringBuilder();

				final JSONObject data = member.getJSONObject("value").getJSONObject("array").getJSONObject("data");

				if (0 == data.length()) {
					throw new Exception("At least one Tag");
				}

				final Object value = data.get("value");

				if (value instanceof JSONArray) {
					final JSONArray tags = (JSONArray) value;

					for (int j = 0; j < tags.length(); j++) {
						final String tagTitle = tags.getJSONObject(j).getString("string");

						tagBuilder.append(tagTitle);

						if (j < tags.length() - 1) {
							tagBuilder.append(",");
						}
					}
				} else {
					final JSONObject tag = (JSONObject) value;

					tagBuilder.append(tag.getString("string"));
				}

				ret.put(Article.ARTICLE_TAGS_REF, tagBuilder.toString());
			}
		}

		final boolean publish = 1 == params.getJSONObject(INDEX_PUBLISH).getJSONObject("value").getInt("boolean");

		ret.put(Article.ARTICLE_IS_PUBLISHED, publish);

		ret.put(Article.ARTICLE_COMMENTABLE, true);
		ret.put(Article.ARTICLE_VIEW_PWD, "");

		return ret;
	}

	/**
	 * Processes {@value #METHOD_GET_RECENT_POSTS}.
	 *
	 * @param fetchSize
	 *            the specified fetch size
	 * @return method response XML
	 * @throws Exception
	 *             exception
	 */
	private String getRecentPosts(final int fetchSize) throws Exception {
		final StringBuilder stringBuilder = new StringBuilder(
				"<?xml version=\"1.0\" encoding=\"UTF-8\"?><methodResponse><params><param><value><array><data>");

		final String posts = buildRecentPosts(fetchSize);

		stringBuilder.append(posts);

		stringBuilder.append("</data></array></value></param></params></methodResponse>");

		return stringBuilder.toString();
	}

	/**
	 * Processes {@value #METHOD_GET_CATEGORIES}.
	 *
	 * @return method response XML
	 * @throws Exception
	 *             exception
	 */
	private String getCategories() throws Exception {
		final StringBuilder stringBuilder = new StringBuilder(
				"<?xml version=\"1.0\" encoding=\"UTF-8\"?><methodResponse><params><param><value><array><data>");

		final String categories = buildCategories();

		stringBuilder.append(categories);

		stringBuilder.append("</data></array></value></param></params></methodResponse>");

		return stringBuilder.toString();
	}

	/**
	 * Processes {@value #METHOD_GET_USERS_BLOGS}.
	 *
	 * @return method response XML
	 * @throws Exception
	 *             exception
	 */
	private String getUsersBlogs() throws Exception {
		final StringBuilder stringBuilder = new StringBuilder(
				"<?xml version=\"1.0\" encoding=\"UTF-8\"?><methodResponse><params><param><value><array><data><value><struct>");

		final JSONObject preference = preferenceQueryService.getPreference();
		final String blogInfo = buildBlogInfo(preference);

		stringBuilder.append(blogInfo);

		stringBuilder.append("</struct></value></data></array></value></param></params></methodResponse>");

		return stringBuilder.toString();
	}

	/**
	 * Builds a post (post struct) with the specified post id.
	 *
	 * @param postId
	 *            the specified post id
	 * @return blog info XML
	 * @throws Exception
	 *             exception
	 */
	private String buildPost(final String postId) throws Exception {
		final StringBuilder stringBuilder = new StringBuilder();

		final JSONObject result = articleQueryService.getArticle(postId);

		if (null == result) {
			throw new Exception("Not found article[id=" + postId + "]");
		}

		final JSONObject article = result.getJSONObject(Article.ARTICLE);

		final Date createDate = (Date) article.get(Article.ARTICLE_CREATE_DATE);
		final String articleTitle = StringEscapeUtils.escapeXml(article.getString(Article.ARTICLE_TITLE));

		stringBuilder.append("<struct>");

		stringBuilder.append("<member><name>dateCreated</name>").append("<value><dateTime.iso8601>")
				.append(DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT.format(createDate))
				.append("</dateTime.iso8601></value></member>");

		stringBuilder.append("<member><name>description</name>").append("<value>")
				.append(StringEscapeUtils.escapeXml(article.getString(Article.ARTICLE_CONTENT)))
				.append("</value></member>");

		stringBuilder.append("<member><name>title</name>").append("<value>").append(articleTitle)
				.append("</value></member>");

		stringBuilder.append("<member><name>categories</name>").append("<value><array><data>");
		final JSONArray tags = article.getJSONArray(Article.ARTICLE_TAGS_REF);

		for (int i = 0; i < tags.length(); i++) {
			final String tagTitle = tags.getJSONObject(i).getString(Tag.TAG_TITLE);

			stringBuilder.append("<value>").append(tagTitle).append("</value>");
		}
		stringBuilder.append("</data></array></value></member></struct>");

		return stringBuilder.toString();
	}

	/**
	 * Builds recent posts (array of post structs) with the specified fetch
	 * size.
	 *
	 * @param fetchSize
	 *            the specified fetch size
	 * @return blog info XML
	 * @throws Exception
	 *             exception
	 */
	private String buildRecentPosts(final int fetchSize) throws Exception {

		final StringBuilder stringBuilder = new StringBuilder();

		final List<JSONObject> recentArticles = articleQueryService.getRecentArticles(fetchSize);

		for (final JSONObject article : recentArticles) {
			final Date createDate = (Date) article.get(Article.ARTICLE_CREATE_DATE);
			final String articleTitle = StringEscapeUtils.escapeXml(article.getString(Article.ARTICLE_TITLE));

			stringBuilder.append("<value><struct>");

			stringBuilder.append("<member><name>dateCreated</name>").append("<value><dateTime.iso8601>")
					.append(DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT.format(createDate))
					.append("</dateTime.iso8601></value></member>");

			stringBuilder.append("<member><name>description</name>").append("<value>")
					.append(StringEscapeUtils.escapeXml(article.getString(Article.ARTICLE_CONTENT)))
					.append("</value></member>");

			stringBuilder.append("<member><name>title</name>").append("<value>").append(articleTitle)
					.append("</value></member>");

			stringBuilder.append("<member><name>postid</name>").append("<value>")
					.append(article.getString(Keys.OBJECT_ID)).append("</value></member>");

			stringBuilder.append("<member><name>categories</name>").append("<value><array><data>");
			final String tagTitles = article.getString(Article.ARTICLE_TAGS_REF);
			final String[] tagTitleArray = tagTitles.split(",");

			for (int i = 0; i < tagTitleArray.length; i++) {
				final String tagTitle = tagTitleArray[i];

				stringBuilder.append("<value>").append(tagTitle).append("</value>");
			}
			stringBuilder.append("</data></array></value></member>");

			stringBuilder.append("</struct></value>");
		}

		return stringBuilder.toString();
	}

	/**
	 * Builds categories (array of category info structs) with the specified
	 * preference.
	 *
	 * @return blog info XML
	 * @throws Exception
	 *             exception
	 */
	private String buildCategories() throws Exception {
		final StringBuilder stringBuilder = new StringBuilder();

		final List<JSONObject> tags = tagQueryService.getTags();

		for (final JSONObject tag : tags) {
			final String tagTitle = StringEscapeUtils.escapeXml(tag.getString(Tag.TAG_TITLE));
			final String tagId = tag.getString(Keys.OBJECT_ID);

			stringBuilder.append("<value><struct>");

			stringBuilder.append("<member><name>description</name>").append("<value>").append(tagTitle)
					.append("</value></member>");

			stringBuilder.append("<member><name>title</name>").append("<value>").append(tagTitle)
					.append("</value></member>");

			stringBuilder.append("<member><name>categoryid</name>").append("<value>").append(tagId)
					.append("</value></member>");

			stringBuilder.append("<member><name>htmlUrl</name>").append("<value>").append(Latkes.getServePath())
					.append("/tags/").append(tagTitle).append("</value></member>");

			stringBuilder.append("<member><name>rsslUrl</name>").append("<value>").append(Latkes.getServePath())
					.append("/tag-articles-rss.do?oId=").append(tagId).append("</value></member>");
			stringBuilder.append("</struct></value>");
		}

		return stringBuilder.toString();
	}

	/**
	 * Builds blog info struct with the specified preference.
	 *
	 * @param preference
	 *            the specified preference
	 * @return blog info XML
	 * @throws JSONException
	 *             json exception
	 */
	private String buildBlogInfo(final JSONObject preference) throws JSONException {
		final String blogId = preference.getString(Keys.OBJECT_ID);

		final String blogTitle = StringEscapeUtils.escapeXml(preference.getString(Option.ID_C_BLOG_TITLE));

		final StringBuilder stringBuilder = new StringBuilder("<member><name>blogid</name><value>").append(blogId)
				.append("</value></member>");

		stringBuilder.append("<member><name>url</name><value>").append(Latkes.getServePath())
				.append("</value></member>");
		stringBuilder.append("<member><name>blogName</name><value>").append(blogTitle).append("</value></member>");

		return stringBuilder.toString();
	}
}
